package com.iteaj.iot.plc.melsec;

import com.github.xingshuangs.iot.common.buff.ByteWriteBuff;
import com.github.xingshuangs.iot.protocol.melsec.enums.EMcFrameType;
import com.github.xingshuangs.iot.protocol.melsec.model.*;
import com.iteaj.iot.client.protocol.ClientSocketProtocol;
import com.iteaj.iot.consts.ExecStatus;
import com.iteaj.iot.plc.*;
import com.iteaj.iot.utils.ByteUtil;

import java.util.ArrayList;
import java.util.List;

/**
 * 基于tcp实现的操作欧姆龙PLC设备的协议
 */
public class MelsecTcpProtocol extends PlcClientProtocol<MelsecMessage> {

    private byte[] data;

    private MelsecConnectProperties connectProperties;

    /**
     * 使用默认的客户端
     */
    public MelsecTcpProtocol() {
        super(2);
    }

    /**
     * 使用指定配置的客户端
     *
     * @param properties
     */
    public MelsecTcpProtocol(MelsecConnectProperties properties) {
        super(properties, 2);
        this.connectProperties = properties;
    }

    @Override
    public ClientSocketProtocol buildRequestMessage() {
        if (this.requestMessage() != null) {
            return this;
        }

        return super.buildRequestMessage();
    }

    @Override
    protected MelsecMessage doBuildRequestMessage() {

        McMessageReq messageReq = new McMessageReq();
        if (this.getWriteAddress() == null) { // 读plc请求
            if (this.getBatchAddress().size() == 1) { // 读取一个地址
                String address = this.getBatchAddress().get(0).getAddress();
                int count = this.getBatchAddress().get(0).getLength();
                int newCount = count % 2 == 0 ? (count / 2) : ((count + 1) / 2);
                McDeviceAddress deviceAddress = McDeviceAddress.createBy(address, newCount);
                messageReq = McReqBuilder.createReadDeviceBatchInWordReq(deviceAddress);
            }
        } else { // 写plc请求
            String address = this.getWriteAddress().getAddress();
            byte[] newData = data;
            if (data.length % 2 != 0) {
                newData = ByteWriteBuff.newInstance(data.length + 1, true).putBytes(data).getData();
            }
            // 软元件按字批量读取，是字的个数，而不是字节个数
            McDeviceContent deviceContent = McDeviceContent.createBy(address, newData.length / 2, newData);
            messageReq = McReqBuilder.createWriteDeviceBatchInWordReq(deviceContent);
        }
        MelsecMessageHeader messageHeader = new MelsecMessageHeader();

        if (connectProperties.getModel().equals(MelsecModel.A)) {
            messageHeader.setMessage(MelsecMessageHeader.buildRequestHeader(EMcFrameType.FRAME_1E, McFrame1EAccessRoute.createDefault(), 3000).getMessage());
        } else {
            messageHeader.setMessage(MelsecMessageHeader.buildRequestHeader(EMcFrameType.FRAME_3E, McFrame4E3EAccessRoute.createDefault(), 3000).getMessage());
        }
        MelsecMessageBody body = new MelsecMessageBody(messageReq.getData().toByteArray());
        MelsecMessage message = new MelsecMessage(messageHeader, body);
        message.setMessage(messageReq.toByteArray());
        return message;
    }

    @Override
    public void doBuildResponseMessage(MelsecMessage responseMessage) {
        if (getExecStatus() == ExecStatus.success) {


            byte[] message = responseMessage.getMessage();
            McMessageAck ack = null;
            if (connectProperties.getModel().equals(MelsecModel.A)) {
                ack = McMessageAck.fromBytes(message, EMcFrameType.FRAME_1E);
            } else {
                ack = McMessageAck.fromBytes(message, EMcFrameType.FRAME_3E);
            }
            //ack.selfCheck();
            try {
                if (ack.getHeader().getEndCode() != 0) {
                    String errorContent = this.extractError(ack.getHeader().getEndCode());
                    // 响应返回异常，异常码
                    String errorStr = String.format("错误信息:%d，%s", ack.getHeader().getEndCode(), errorContent);
                    this.data = ack.getData().toByteArray();
                    this.setCmdStatus(false, errorStr);
                } else {
                    this.data = ack.getData().toByteArray();
                    this.setCmdStatus(true, "操作成功");
                }

            } catch (PlcException e) {
                this.setCmdStatus(false, e.getMessage());
            }
        }
    }

    /**
     * Extract error
     * (提取错误信息)
     *
     * @param errorCode error code
     * @return error string
     */
    private String extractError(int errorCode) {
        switch (errorCode) {
            case 0xC050:
                // 在"通信数据代码设置"中，设置ASCII代码通信时，接收了无法转换为二进制代码的ASCII代码的数据
                return "In Communication Data Code Settings, when you set up ASCII code communication, you received data in ASCII code that could not be converted to binary code";
            case 0xC051:
            case 0xC052:
            case 0xC053:
            case 0xC054:
                // 写入或读取点数超出了允许范围
                return "The number of write or read points exceeds the allowed range";
            case 0xC056:
                // 写入及读取请求超出了最大地址
                return "Write and read requests exceeded the maximum address";
            case 0xC058:
                // ASCII-二进制转换后的请求数据长度与字符部分的数据数不一致
                return "The length of the requested data after the ASCII-binary conversion is inconsistent with the number of data in the character part";
            case 0xC059:
                // 指令、子指令的指定中有错误
                return "There is an error in the assignment of instruction or subinstruction";
            case 0xC05B:
                // CPU模块无法对指定软元件尽心写入及读取
                return "The CPU module cannot write and read the specified software component";
            case 0xC05C:
                // 请求内容中有错误。（对字软元件进行了以位为单位的写入及读取等）
                return "There is an error in the request content.(The word software components are written and read in bit units, etc.)";
            case 0xC05D:
                // 未进行监视登录
                return "No monitored login";
            case 0xC05F:
                // 是无法对对象CPU模块执行的请求
                return "The request to the object CPU module could not be executed";
            case 0xC060:
                // 请求内容中有错误。（对位软元件的数据指定中有错误）
                return "There is an error in the request content.(There is an error in the data assignment of the alignment software component)";
            case 0xC061:
                // 请求数据长度与字符部分的数据数不一致
                return "The requested data length does not match the number of data in the character section";
            case 0xC0B5:
                // 指定了CPU模块中无法处理的数据
                return "Specifies data that cannot be processed in the CPU module";
            default:
                // 请查询三菱用户手册进行错误解析
                return "Please consult the Mitsubishi user manual for error resolution: " + errorCode;
        }
    }

    @Override
    public DataTransfer getDataTransfer() {
        return MelsecDataTransfer.getInstance();
    }

    /**
     * 批量读取
     *
     * @param batchAddress
     * @return
     * @see ReadAddress#getAddress() M100, I100, Q100, DB1.100
     */
    @Override
    protected List<byte[]> doRead(List<ReadAddress> batchAddress) {
        List<byte[]> result = new ArrayList<>();
        int start = 0;
        if (this.data != null) {
            for (int index = 0; index < batchAddress.size(); index++) {
                ReadAddress address = batchAddress.get(index);

                byte[] bytes = ByteUtil.subBytes(this.data, start, start + address.getLength());

                if (address.getType() == AddressType.Bit) {
                    start += address.getLength() + 5;
                } else {
                    start += address.getLength() + 4;
                }

                result.add(bytes);
            }
        }

        return result;
    }

    @Override
    protected short calcBitLength(short length) {
        return (short) (length / 8 + (length % 8 > 0 ? 1 : 0));
    }

    @Override
    protected boolean[] bytesToBooleans(byte[] bytes, short length) {
        boolean[] booleans = PLCUtils.byteToBoolArray(bytes);
        return PLCUtils.copeArray(booleans, 0, length);
    }


    @Override
    public PlcProtocolType protocolType() {
        return PlcProtocolType.Melsec;
    }


    @Override
    public void writeFull(byte[] fullMessage) {
        this.requestMessage = new MelsecMessage(new MelsecMessageHeader(fullMessage));
        this.sync(this.getTimeout()).request();
    }

    @Override
    public byte[] readFull(byte[] fullMessage) {
        this.requestMessage = new MelsecMessage(new MelsecMessageHeader(fullMessage));
        this.sync(this.getTimeout()).request();
        return this.data;
    }

    @Override
    public void write(String address, boolean[] value) {
        byte[] bytes = ByteUtil.boolArrayToByte(value);
        this.write(address, bytes);
    }

    public byte[] getData() {
        return data;
    }

}
